package fts.ftsmonitoring.impl;

import static java.nio.file.StandardWatchEventKind.OVERFLOW;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKind;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import fts.ftsmonitoring.FileAction;
import fts.ftsmonitoring.FileMonitoringRoot;
import fts.ftsmonitoring.IFileMonitoringService;
import fts.ftsmonitoring.IFileObserver;
import fts.ftsmonitoring.MonitoringStatus;

/**
 * @author Igor Zilberman (304033327)
 * 
 *         Implementation of IFileMonitoringService. Uses JRE7 utilization of OS
 *         file change notification whenever possible Currently supports only
 *         CREATE/DELETE notifications
 */
public class JSRFileMonitoringService implements IFileMonitoringService
{

	private static JSRFileMonitoringService instance;

	/**
	 * @return File Monitoring Service
	 * @throws IOException
	 */
	public static JSRFileMonitoringService getInstance() throws IOException
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);
		if (instance == null)
		{
			synchronized (JSRFileMonitoringService.class)
			{
				if (instance == null)
				{
					instance = new JSRFileMonitoringService();
				}
			}
		}
		return instance;
	}

	/**
	 * nio watcher service
	 */
	private WatchService watcherSvc;
	/**
	 * file visitor - responsible of watcher service attachment to directory
	 * tree
	 */
	private JSRFileVisitor jsrVisitor;

	/**
	 * mapping of root path to file pattern to watch
	 */
	private HashMap<Path, String> pathToPatterns;

	/**
	 * observers list to accept notifications of file creation/deletion
	 */
	private ArrayList<IFileObserver> observers;

	/**
	 * private constructor
	 * 
	 * @throws IOException
	 */
	private MonitoringStatus status;
	
	private JSRFileMonitoringService() throws IOException
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		watcherSvc = FileSystems.getDefault().newWatchService();
		jsrVisitor = new JSRFileVisitor(watcherSvc);
		observers = new ArrayList<IFileObserver>();
		status = MonitoringStatus.STOPPED;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fts.ftsmonitoring.IFileMonitoringService#RegisterObserver(fts.ftsmonitoring
	 * .IFileObserver)
	 */
	public synchronized void registerFileObserver(IFileObserver observer)
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		observers.add(observer);
	}

	@SuppressWarnings("unchecked")
	private static <T> WatchEvent<T> castEvent(WatchEvent<?> event)
	{
		return (WatchEvent<T>) event;
	}

	/**
	 * flag indication of attempt to stop the service
	 */
	private AtomicBoolean tryStop = new AtomicBoolean(false);

	/*
	 * (non-Javadoc)
	 * 
	 * @see fts.ftsmonitoring.IFileMonitoringService#StopMonitoring()
	 */
	public void stopMonitoring()
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		tryStop.set(true);
		status = MonitoringStatus.STOPPING;
		notifyStatusChange();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fts.ftsmonitoring.IFileMonitoringService#UnregisterObserver(fts.ftsmonitoring
	 * .IFileObserver)
	 */
	public synchronized void unregisterFileObserver(IFileObserver observer)
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		observers.remove(observer);
	}

	private synchronized void notifyStatusChange()
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		for (IFileObserver observer : observers)
		{
			try
			{
				observer.onMonitoringStatusChange(status);
			}
			catch (Exception ex)
			{

			}
		}
	}
	
	private void notifyFileChange(String src, String dest, FileAction action)
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		for (IFileObserver observer : observers)
		{
			try
			{
				observer.onFileChange(src, dest, action);
			}
			catch (Exception ex)
			{

			}
		}
	}
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fts.ftsmonitoring.IFileMonitoringService#StartMonitoring(java.util.Collection
	 * )
	 */
	public void startMonitoring(Collection<FileMonitoringRoot> roots) throws InterruptedException
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);
		tryStop.set(false);
		if(status != MonitoringStatus.STOPPED)
		{
			return;
		}
		status = MonitoringStatus.STARTING;
		notifyStatusChange();
		startWatchers(roots);
		status = MonitoringStatus.STARTED;
		notifyStatusChange();
		try
		{
			while (!tryStop.get())
			{
				WatchKey watchKey;
				watchKey = watcherSvc.poll(500, TimeUnit.MILLISECONDS);
				if (watchKey != null)
				{
					for (WatchEvent<?> event : watchKey.pollEvents())
					{
						WatchEvent<Path> watchEvent = castEvent(event);

						if (event.kind().equals(OVERFLOW))
						{
							continue;
						}

						Path prefixPath = jsrVisitor.getPath(watchKey);

						if (prefixPath != null)
						{
							Path absPath = prefixPath.resolve(watchEvent.context());
							File changedFile = new File(absPath.toUri());

							if (changedFile.isDirectory() && event.kind().equals(StandardWatchEventKind.ENTRY_CREATE))
							{
								Files.walkFileTree(absPath, jsrVisitor);

								Path[] pathChange;
								while ((pathChange = jsrVisitor.getPathChange()) != null)
								{
									notifyFileChange(pathChange[0].toString(), pathChange[1].toString(), FileAction.MOVE);
								}
							}
							else
							{
								Set<Path> paths = pathToPatterns.keySet();
								Iterator<Path> iter = paths.iterator();
								Path root = null;
								int level = 0;
								while (iter.hasNext())
								{
									Path curr = iter.next();
									int currLvl = curr.getNameCount();
									if (prefixPath.startsWith(curr) && currLvl >= level)
									{
										level = currLvl;
										root = curr;
									}
								}
								if (root != null)
								{

									if (watchEvent.context().toString().matches(pathToPatterns.get(root)))
									{
										FileAction action = null;
										if (watchEvent.kind().equals(StandardWatchEventKind.ENTRY_DELETE))
										{
											action = FileAction.DELETE;
										}
										else if (watchEvent.kind().equals(StandardWatchEventKind.ENTRY_CREATE))
										{
											action = FileAction.NEW;
										}
										if (action != null)
										{
											notifyFileChange(absPath.toString(), absPath.toString(), action);
										}

									}
								}
							}
						}
						watchKey.reset();
					}
				}
			}
		}
		catch (InterruptedException ex)
		{
			throw ex;
		}
		finally
		{
			jsrVisitor.stopWatchers();
			pathToPatterns.clear();
			tryStop.set(false);
			status = MonitoringStatus.STOPPED;
			notifyStatusChange();
		}
	}

	/**
	 * @param roots
	 *            Root directories to watch
	 */
	private void startWatchers(Collection<FileMonitoringRoot> roots)
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);
		pathToPatterns = new HashMap<Path, String>();
		if(roots != null && roots.size() > 0)
		{
				Iterator<FileMonitoringRoot> rootsIter = roots.iterator();
		
				while (rootsIter.hasNext())
				{
					FileMonitoringRoot root = rootsIter.next();
					File _dir = new File(root.getRootPath());
		
					if (_dir.exists() && _dir.isDirectory())
					{
						Path directotyToWatch = _dir.toPath();
		
						if (!pathToPatterns.containsKey(directotyToWatch))
						{
							pathToPatterns.put(directotyToWatch, root.getFilePatternsExpression());
						}
		
						Files.walkFileTree(directotyToWatch, jsrVisitor);
					}
				}
		}
	}

	public MonitoringStatus getStatus()
	{
		System.out.println(Thread.currentThread().getStackTrace()[1]);

		return status;
	}

}
